{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "fe12c203-e6a6-452c-a655-afb8a03a4ff5",
   "metadata": {},
   "source": [
    "# End of week 1 exercise\n",
    "\n",
    "To demonstrate your familiarity with OpenAI API, and also Ollama, build a tool that takes a technical question,  \n",
    "and responds with an explanation. This is a tool that you will be able to use yourself during the course!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "c1070317-3ed9-4659-abe3-828943230e03",
   "metadata": {},
   "outputs": [],
   "source": [
    "# imports\n",
    "import os\n",
    "from dotenv import load_dotenv\n",
    "from openai import OpenAI\n",
    "from IPython.display import Markdown, display, update_display\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "4a456906-915a-4bfd-bb9d-57e505c5093f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# constants\n",
    "\n",
    "MODEL_GPT = 'gpt-4o-mini'\n",
    "MODEL_LLAMA = 'llama3.2'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "a8d7923c-5f28-4c30-8556-342d7c8497c1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# set up environment\n",
    "load_dotenv(override=True)\n",
    "openai = OpenAI()\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "3f0d0137-52b0-47a8-81a8-11a90a010798",
   "metadata": {},
   "outputs": [],
   "source": [
    "# here is the question; type over this to ask something new\n",
    "\n",
    "question = \"\"\"\n",
    "Please explain what this code does and why:\n",
    "yield from {book.get(\"author\") for book in books if book.get(\"author\")}\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "c3732f12",
   "metadata": {},
   "outputs": [],
   "source": [
    "system_prompt= \"\"\"\n",
    "You are a highly skilled programming assistant. \n",
    "Your task is to explain any code provided by the user clearly, accurately, and concisely. \n",
    "Focus on purpose, logic, and execution flow. Avoid unnecessary commentary and only describe what the code does and how it works.\n",
    "Response in Markdown.\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "60ce7000-a4a5-4cce-a261-e75ef45063b4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "This code is a generator expression that yields values from a set created based on the `books` collection. Here's a breakdown of its purpose and execution flow:\n",
       "\n",
       "1. **Set Comprehension**:\n",
       "   - The `{book.get(\"author\") for book in books if book.get(\"author\")}` part creates a set of authors. It iterates over each `book` in the `books` iterable.\n",
       "\n",
       "2. **Filtering**:\n",
       "   - The condition `if book.get(\"author\")` ensures that only books with a non-`None` or non-empty author value are included in the set. This means if a book does not have an author specified, it will be skipped.\n",
       "\n",
       "3. **Yielding Values**:\n",
       "   - The `yield from` statement is used to yield each value from the resulting set of authors. This allows the surrounding function to act as a generator, returning each author one by one.\n",
       "\n",
       "4. **Purpose**:\n",
       "   - The overall purpose of this code is to provide a generator that produces a unique collection of book authors, efficiently skipping any entries that lack an author.\n",
       "\n",
       "In summary, this code is designed to produce a unique list of authors from a collection of book entries, omitting any entries that do not have an associated author."
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Get gpt-4o-mini to answer, with streaming\n",
    "stream = openai.chat.completions.create(\n",
    "    model=MODEL_GPT,\n",
    "    messages=[\n",
    "        {\"role\":\"system\", \"content\": system_prompt},\n",
    "        {\"role\":\"user\", \"content\": question}\n",
    "        ],\n",
    "    stream=True\n",
    ")\n",
    "response = \"\"\n",
    "display_value = display(Markdown(\"\"), display_id=True)\n",
    "\n",
    "for chunk in stream:\n",
    "    response += chunk.choices[0].delta.content or ''\n",
    "    update_display(Markdown(response), display_id=display_value.display_id)\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "8f7c8ea8-4082-4ad0-8751-3301adcf6538",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "## What the statement does  \n",
       "\n",
       "```python\n",
       "yield from {book.get(\"author\") for book in books if book.get(\"author\")}\n",
       "```\n",
       "\n",
       "1. **Creates a set**  \n",
       "   ```python\n",
       "   {book.get(\"author\") for book in books if book.get(\"author\")}\n",
       "   ```  \n",
       "   * Iterates over the iterable `books`.  \n",
       "   * For each `book` dictionary it calls `book.get(\"author\")`.  \n",
       "   * The `if book.get(\"author\")` guard filters out entries where the `\"author\"` key is missing or its value is falsy (`None`, `''`, `0`, etc.).  \n",
       "   * The result of `book.get(\"author\")` is added to a **set**, which automatically removes duplicates.  \n",
       "   After the comprehension finishes you have a set of **unique, non‑empty author names**.\n",
       "\n",
       "2. **Yields each element of that set**  \n",
       "   `yield from <iterable>` is a shortcut inside a generator function that forwards (or “delegates”) all values produced by the given iterable to the caller of the generator.  \n",
       "   In this case it iterates over the set produced in step 1 and yields each author — one at a time.\n",
       "\n",
       "So, when this line appears inside a generator function, the function will output each distinct author found in `books`, skipping books that lack an author entry.\n",
       "\n",
       "---\n",
       "\n",
       "## Why it is written this way  \n",
       "\n",
       "| Reason | Explanation |\n",
       "|--------|-------------|\n",
       "| **`yield from`** | Removes the need for an explicit loop (`for a in authors: yield a`). It makes the generator concise and clearly expresses “forward everything from this iterable”. |\n",
       "| **Set comprehension** (`{ … }`) | Guarantees that each author is yielded only once, because sets cannot contain duplicates. |\n",
       "| **`book.get(\"author\")` with a guard** | Safely accesses the `\"author\"` key (no `KeyError`) and filters out falsy values, ensuring the set contains only meaningful author strings. |\n",
       "| **Combined in one line** | Provides a compact, readable one‑liner that both extracts the unique authors and yields them in a generator context. |\n",
       "\n",
       "---\n",
       "\n",
       "## Execution flow (step‑by‑step)\n",
       "\n",
       "1. The generator function is called; execution stops at the `yield from` line.\n",
       "2. The set comprehension runs:\n",
       "   - For each `book` in `books`:\n",
       "     - `author = book.get(\"author\")`\n",
       "     - If `author` is truthy, add it to the temporary set.\n",
       "3. Once the set is built, `yield from` obtains an iterator over that set.\n",
       "4. The generator yields each `author` value in arbitrary order (the order of a set is not guaranteed).\n",
       "5. When the set is exhausted, the `yield from` statement finishes and control returns to the caller of the generator.\n",
       "\n",
       "---\n",
       "\n",
       "### Quick example\n",
       "\n",
       "```python\n",
       "def authors(books):\n",
       "    yield from {b.get(\"author\") for b in books if b.get(\"author\")}\n",
       "\n",
       "books = [\n",
       "    {\"title\": \"A\", \"author\": \"Alice\"},\n",
       "    {\"title\": \"B\", \"author\": \"Bob\"},\n",
       "    {\"title\": \"C\", \"author\": \"Alice\"},   # duplicate author\n",
       "    {\"title\": \"D\"}                       # no author key\n",
       "]\n",
       "\n",
       "print(list(authors(books)))   # → ['Alice', 'Bob'] (order may vary)\n",
       "```\n",
       "\n",
       "The function returns a generator that yields `\"Alice\"` and `\"Bob\"` once each, ignoring the duplicate and the book without an author."
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Get Llama 3.2 to answer\n",
    "from ollama import Client\n",
    "\n",
    "api_key = os.environ.get('OLLAMA_API_KEY')\n",
    "OLLAMA_BASE_URL = \"https://www.ollama.com\"\n",
    "\n",
    "ollama = Client(\n",
    "    host=OLLAMA_BASE_URL,\n",
    "    headers={'Authorization': 'Bearer ' + api_key}\n",
    ")\n",
    "\n",
    "stream = ollama.chat(\n",
    "    model='gpt-oss:120b',\n",
    "    messages=[\n",
    "        {\"role\":\"system\", \"content\": system_prompt},\n",
    "        {\"role\":\"user\", \"content\": question}\n",
    "        ],\n",
    "    stream=True\n",
    ")\n",
    "\n",
    "response = \"\"\n",
    "display_value = display(Markdown(\"\"), display_id=True)\n",
    "\n",
    "for chunk in stream:\n",
    "    response += chunk['message']['content']\n",
    "    update_display(Markdown(response), display_id=display_value.display_id)\n",
    "\n",
    "# for chunk in stream:\n",
    "#     response += chunk.choices[0].delta.content or ''\n",
    "#     update_display(Markdown(response), display_id=display_value.display_id)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
